Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Raw β€’ Download

feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/ota/dfu/SecureDfuTransport.kt renovate/actions-setup-java-5.x (f17e586c) Text, 33.75 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(
Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffThrowsCountTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffSwallowedExceptionTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4,
Tb4b4b4)

Tff7b72package T7ee787org.meshtastic.feature.firmware.ota.dfu

Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.CoroutineDispatcher
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.TimeoutCancellationException
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.channels.Channel
Tff7b72import T7ee787kotlinx.coroutines.coroutineScope
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.catch
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.withTimeout
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionState
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanner
Tff7b72import T7ee787org.meshtastic.core.ble.BleWriteType
Tff7b72import T7ee787org.meshtastic.core.ble.DEFAULT_BLE_WRITE_VALUE_LENGTH
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleDevice
Tff7b72import T7ee787org.meshtastic.core.common.util.safeCatching
Tff7b72import T7ee787org.meshtastic.feature.firmware.ota.calculateMacPlusOne
Tff7b72import T7ee787org.meshtastic.feature.firmware.ota.scanForBleDevice
Tff7b72import T7ee787kotlin.time.Duration
Tff7b72import T7ee787kotlin.time.Duration.Companion.milliseconds
Tff7b72import T7ee787kotlin.time.Duration.Companion.minutes
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds
Tff7b72import T7ee787kotlin.uuid.Uuid

T8b949e/**
* Kable-based transport for the Nordic Secure DFU (Secure DFU over BLE) protocol.
*
* Usage:
* 1. [triggerButtonlessDfu] β€” connect to the device in normal mode and trigger reboot into DFU mode.
* 2. [connectToDfuMode] β€” scan for the device in DFU mode and establish the DFU GATT session.
* 3. [transferInitPacket] / [transferFirmware] β€” send .dat then .bin.
* 4. [abort] β€” send ABORT to the device before closing (on cancellation or error).
* 5. [close] β€” tear down the connection.
*/
Tff7b72class T56d364SecureDfuTransportTb4b4b4(
Tff7b72private Tff7b72val Te6edf3scannerTb4b4b4: Te6edf3BleScannerTb4b4b4,
Te6edf3connectionFactoryTb4b4b4: Te6edf3BleConnectionFactoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3dispatcherTb4b4b4: Te6edf3CoroutineDispatcherTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3DfuUploadTransport Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3transportScope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3SupervisorJobTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3dispatcherTb4b4b4)
Tff7b72private Tff7b72val Te6edf3bleConnection Tff7b72= Te6edf3connectionFactoryTb4b4b4.Te6edf3createTb4b4b4(Te6edf3transportScopeTb4b4b4, Ta5d6ff"Ta5d6ffSecure DFUTa5d6ff"Tb4b4b4)

T8b949e/** Receives binary notifications from the Control Point characteristic. */
Tff7b72private Tff7b72val Te6edf3notificationChannel Tff7b72= Te6edf3ChannelTff7b72<Te6edf3ByteArrayTff7b72>Tb4b4b4(Te6edf3ChannelTb4b4b4.Te6edf3UNLIMITEDTb4b4b4)

T8b949e// ---------------------------------------------------------------------------
T8b949e// Phase 1: Buttonless DFU trigger (normal-mode device)
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Connects to the device running normal firmware and writes to the Buttonless DFU characteristic so the bootloader
* takes over. The device disconnects and reboots.
*
* Per the Nordic Secure DFU spec, indications **must** be enabled on the Buttonless DFU characteristic before
* writing the Enter DFU command. The device validates the CCCD and rejects the write with
* `ATTERR_CPS_CCCD_CONFIG_ERROR` if indications are not enabled.
*
* After writing the trigger, the device may disconnect before the indication response arrives β€” this race condition
* is expected and handled gracefully.
*
* The caller must have already released the mesh-service BLE connection before calling this.
*/
Tff7b72suspend Tff7b72fun Td2a8fftriggerButtonlessDfuTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3safeCatching Tb4b4b4{
T8b949e// No scan needed: the address is already bonded with the OS, so we can connect directly the same way the
T8b949e// Nordic Android DFU library does (BluetoothAdapter.getRemoteDevice(address).connectGatt). Scanning here is
T8b949e// unreliable because the device may not have resumed advertising in the brief window after we released the
T8b949e// mesh-service GATT.
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Connecting to Tffd700$Te6edf3addressTa5d6ff to trigger buttonless DFU...Ta5d6ff" Tb4b4b4}
Te6edf3bleConnectionTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3MeshtasticBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4, Te6edf3CONNECT_TIMEOUTTb4b4b4)

T8b949e// Try the Nordic Secure DFU service (FE59) first β€” used when the firmware is built with BLE_DFU_SECURE.
T8b949e// If it isn't exposed, fall back to the Legacy DFU service (1530) used by Meshtastic builds that rely on
T8b949e// Adafruit's stock BLEDfu helper. Both ultimately reboot the device into the same bootloader.
Tff7b72try Tb4b4b4{
Te6edf3triggerSecureButtonlessTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3NoSuchElementExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3iTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Secure DFU service (FE59) not present β€” falling back to legacy DFU service (1530)Ta5d6ff" Tb4b4b4}
Te6edf3triggerLegacyButtonlessTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e// Device will disconnect and reboot β€” expected, not an error.
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Buttonless DFU triggered, device is rebooting...Ta5d6ff" Tb4b4b4}
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/** Nordic Secure DFU buttonless trigger β€” INDICATE + write 0x01, then wait for `0x20-01-STATUS` response. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fftriggerSecureButtonlessTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3triggerButtonlessTb4b4b4(
Te6edf3serviceUuid Tff7b72= Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4,
Te6edf3characteristicUuid Tff7b72= Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3BUTTONLESS_NO_BONDSTb4b4b4,
Te6edf3payload Tff7b72= Te6edf3byteArrayOfTb4b4b4(Te6edf3BUTTONLESS_ENTER_BOOTLOADERTb4b4b4)Tb4b4b4,
Te6edf3profileTimeout Tff7b72= Te6edf3TRIGGER_TIMEOUTTb4b4b4,
Te6edf3logLabel Tff7b72= Ta5d6ff"Ta5d6ffsecureTa5d6ff"Tb4b4b4,
Te6edf3awaitResponse Tff7b72= Tb4b4b4{ Te6edf3channel Tff7b72-Tff7b72>
Tff7b72try Tb4b4b4{
Te6edf3withTimeoutTb4b4b4(Te6edf3BUTTONLESS_RESPONSE_TIMEOUTTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3response Tff7b72= Te6edf3channelTb4b4b4.Te6edf3receiveTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(
Te6edf3responseTb4b4b4.Te6edf3size Tff7b72>Tff7b72= T79c0ff3 Tff7b72&Tff7b72&
Te6edf3responseTff7b72[T79c0ff0Tff7b72] Tff7b72=Tff7b72= Te6edf3BUTTONLESS_RESPONSE_CODE Tff7b72&Tff7b72&
Te6edf3responseTff7b72[T79c0ff2Tff7b72] Tff7b72!Tff7b72= T79c0ff0Te6edf3x01Tb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4)
Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Buttonless DFU response indicates error: Tffd700${Te6edf3responseTb4b4b4.Te6edf3toHexStringTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Buttonless DFU indication received successfullyTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3_Tb4b4b4: Te6edf3TimeoutCancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: No buttonless indication received (device may have already disconnected)Ta5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3_Tb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Buttonless indication wait interrupted (device disconnecting)Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Nordic Legacy DFU buttonless trigger (Adafruit `BLEDfu`). Subscribe to NOTIFICATIONS on the control point
* (required to satisfy the device's CCCD check), then write `[0x01, 0x04]` (START_DFU + IMAGE_TYPE_APPLICATION, per
* Nordic's `LegacyButtonlessDfuImpl.java:53`). The device reboots into the bootloader immediately β€” no notification
* response is expected before disconnect.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fftriggerLegacyButtonlessTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3triggerButtonlessTb4b4b4(
Te6edf3serviceUuid Tff7b72= Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4,
Te6edf3characteristicUuid Tff7b72= Te6edf3LegacyDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4,
Te6edf3payload Tff7b72= Te6edf3LEGACY_BUTTONLESS_ENTER_BOOTLOADERTb4b4b4,
Te6edf3profileTimeout Tff7b72= Te6edf3TRIGGER_TIMEOUTTb4b4b4,
Te6edf3logLabel Tff7b72= Ta5d6ff"Ta5d6fflegacyTa5d6ff"Tb4b4b4,
Te6edf3awaitResponse Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Shared scaffold for both Secure and Legacy buttonless triggers. The Adafruit/Nordic protocol is identical in
* shape: enable CCCD on a control characteristic, write a small opcode, optionally await an indication/notification
* response. The whole trigger is wrapped in [profileTimeout] and treats timeout as success β€” by the time we'd time
* out, the device has either rebooted (verified by [connectToDfuMode]) or never received the byte (which surfaces
* as a useful scan failure later). This escapes the well-known race where Kable's `WITH_RESPONSE` write blocks on
* an ATT ACK that never arrives because the device rebooted before sending it.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fftriggerButtonlessTb4b4b4(
Te6edf3serviceUuidTb4b4b4: Te6edf3UuidTb4b4b4,
Te6edf3characteristicUuidTb4b4b4: Te6edf3UuidTb4b4b4,
Te6edf3payloadTb4b4b4: Te6edf3ByteArrayTb4b4b4,
Te6edf3profileTimeoutTb4b4b4: Te6edf3DurationTb4b4b4,
Te6edf3logLabelTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3awaitResponseTb4b4b4: Tb4b4b4(Te6edf3suspend Te6edf3CoroutineScopeTb4b4b4.Tb4b4b4(Te6edf3ChannelTff7b72<Te6edf3ByteArrayTff7b72>Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tff7b72?Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3withTimeoutTb4b4b4(Te6edf3profileTimeoutTb4b4b4) Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3serviceUuidTb4b4b4, Te6edf3timeout Tff7b72= Te6edf3profileTimeoutTb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3char Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3characteristicTb4b4b4(Te6edf3characteristicUuidTb4b4b4)

Tff7b72val Te6edf3channel Tff7b72= Te6edf3ChannelTff7b72<Te6edf3ByteArrayTff7b72>Tb4b4b4(Te6edf3ChannelTb4b4b4.Te6edf3UNLIMITEDTb4b4b4)
Tff7b72val Te6edf3observeJob Tff7b72=
Te6edf3service
Tb4b4b4.Te6edf3observeTb4b4b4(Te6edf3charTb4b4b4)
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3channelTb4b4b4.Te6edf3trySendTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3dTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Tffd700$Te6edf3logLabelTa5d6ff notification stream ended (expected on disconnect)Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)

Te6edf3delayTb4b4b4(Te6edf3SUBSCRIPTION_SETTLETb4b4b4)

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Writing Tffd700$Te6edf3logLabelTa5d6ff buttonless DFU trigger...Ta5d6ff" Tb4b4b4}
Te6edf3serviceTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3charTb4b4b4, Te6edf3payloadTb4b4b4, Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSETb4b4b4)

Te6edf3awaitResponseTff7b72?.Te6edf3invokeTb4b4b4(Tff7b72thisTb4b4b4, Te6edf3channelTb4b4b4)

Te6edf3observeJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3_Tb4b4b4: Te6edf3TimeoutCancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ffDFU: Tffd700$Te6edf3logLabelTa5d6ff buttonless trigger timed out β€” likely cause: stale BLE bond (Meshtastic Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBLEDfu requires SECMODE_ENC_WITH_MITM). User must Forget+Re-pair the device in Android Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBluetooth settings if the next DFU-mode scan also fails.Ta5d6ff"
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Phase 2: Connect to device in DFU mode
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Scans for the device in DFU mode (address or address+1) and establishes the GATT connection, enabling
* notifications on the Control Point.
*/
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffconnectToDfuModeTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3safeCatching Tb4b4b4{
Tff7b72val Te6edf3dfuAddress Tff7b72= Te6edf3calculateMacPlusOneTb4b4b4(Te6edf3addressTb4b4b4)
Tff7b72val Te6edf3targetAddresses Tff7b72= Te6edf3setOfTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3dfuAddressTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Scanning for DFU mode device at Tffd700$Te6edf3targetAddressesTa5d6ff...Ta5d6ff" Tb4b4b4}

Tff7b72val Te6edf3device Tff7b72=
Te6edf3scanForDevice Tb4b4b4{ Te6edf3d Tff7b72-Tff7b72> Te6edf3dTb4b4b4.Te6edf3address Tff7b72in Te6edf3targetAddresses Tb4b4b4}
Tff7b72?: Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffDFU mode device not found. Tried: Tffd700$Te6edf3targetAddressesTa5d6ff"Tb4b4b4)

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Found DFU mode device at Tffd700${Te6edf3deviceTb4b4b4.Te6edf3addressTffd700}Ta5d6ff, connecting...Ta5d6ff" Tb4b4b4}

Te6edf3bleConnectionTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Connection state β†’ Tffd700$Te6edf3itTa5d6ff" Tb4b4b4} Tb4b4b4}Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3transportScopeTb4b4b4)

Tff7b72val Te6edf3connected Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4, Te6edf3CONNECT_TIMEOUTTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3connected Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffFailed to connect to DFU device Tffd700${Te6edf3deviceTb4b4b4.Te6edf3addressTffd700}Ta5d6ff"Tb4b4b4)
Tb4b4b4}

Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3controlChar Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3characteristicTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4)

T8b949e// Subscribe to Control Point notifications before issuing any commands.
T8b949e// launchIn(this) uses connectionScope so the subscription persists beyond this block.
Tff7b72val Te6edf3subscribed Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3service
Tb4b4b4.Te6edf3observeTb4b4b4(Te6edf3controlCharTb4b4b4)
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3bytes Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3subscribedTb4b4b4.Te6edf3isCompletedTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Control Point subscribedTa5d6ff" Tb4b4b4}
Te6edf3subscribedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Tb4b4b4}
Te6edf3notificationChannelTb4b4b4.Te6edf3trySendTb4b4b4(Te6edf3bytesTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3subscribedTb4b4b4.Te6edf3isCompletedTb4b4b4) Te6edf3subscribedTb4b4b4.Te6edf3completeExceptionallyTb4b4b4(Te6edf3eTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Control Point notification errorTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)

Te6edf3delayTb4b4b4(Te6edf3SUBSCRIPTION_SETTLETb4b4b4)
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3subscribedTb4b4b4.Te6edf3isCompletedTb4b4b4) Te6edf3subscribedTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3subscribedTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Connected and ready (Tffd700${Te6edf3deviceTb4b4b4.Te6edf3addressTffd700}Ta5d6ff)Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Phase 3: Init packet transfer (.dat)
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Sends the DFU init packet (`.dat` file). The device verifies this against the bootloader's security requirements
* before accepting firmware.
*
* PRN is explicitly disabled (set to 0) for the init packet per the Nordic DFU library convention β€” the init packet
* is small (<512 bytes, fits in a single object) and does not benefit from flow control.
*/
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8fftransferInitPacketTb4b4b4(Te6edf3initPacketTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3safeCatching Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Transferring init packet (Tffd700${Te6edf3initPacketTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)...Ta5d6ff" Tb4b4b4}
Te6edf3setPrnTb4b4b4(T79c0ff0Tb4b4b4)
Te6edf3transferObjectWithRetryTb4b4b4(Te6edf3DfuObjectTypeTb4b4b4.Te6edf3COMMANDTb4b4b4, Te6edf3initPacketTb4b4b4, Te6edf3onProgress Tff7b72= Tff7b72nullTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Init packet transferred and executed.Ta5d6ff" Tb4b4b4}
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Phase 4: Firmware transfer (.bin)
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Sends the firmware binary (`.bin` file) using the DFU object-transfer protocol.
*
* The binary is split into objects sized by the device's reported maximum object size. After each object the device
* confirms the running CRC-32. On success, the bootloader validates the full image and reboots into the new
* firmware.
*
* @param firmware Raw bytes of the `.bin` file.
* @param onProgress Callback receiving progress in [0.0, 1.0].
*/
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8fftransferFirmwareTb4b4b4(Te6edf3firmwareTb4b4b4: Te6edf3ByteArrayTb4b4b4, Te6edf3onProgressTb4b4b4: Te6edf3suspend Tb4b4b4(Tffa657FloatTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72=
Te6edf3safeCatching Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Transferring firmware (Tffd700${Te6edf3firmwareTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)...Ta5d6ff" Tb4b4b4}
T8b949e// Bump BLE link to high-throughput mode (~7.5 ms interval) before streaming.
T8b949e// Default Android intervals (~30-50 ms) starve the link during sustained DFU and trigger LSTO.
Tff7b72val Te6edf3highPriorityRequested Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3requestHighConnectionPriorityTb4b4b4(Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffSecure DFU: requestHighConnectionPriority -> Tffd700$Te6edf3highPriorityRequestedTa5d6ff" Tb4b4b4}
Te6edf3setPrnTb4b4b4(Te6edf3PRN_INTERVALTb4b4b4)
Te6edf3transferObjectWithRetryTb4b4b4(Te6edf3DfuObjectTypeTb4b4b4.Te6edf3DATATb4b4b4, Te6edf3firmwareTb4b4b4, Te6edf3onProgressTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Firmware transferred and executed.Ta5d6ff" Tb4b4b4}
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Abort & teardown
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Sends the ABORT opcode to the device, instructing it to discard any in-progress transfer and return to an idle
* state. Best-effort β€” never throws.
*
* Call this before [close] when cancelling or recovering from an error so the device doesn't need a power cycle to
* accept a fresh DFU session.
*/
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffabortTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeCatching Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3controlChar Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3characteristicTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3controlCharTb4b4b4, Te6edf3byteArrayOfTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3ABORTTb4b4b4)Tb4b4b4, Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSETb4b4b4)
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Abort sent to device.Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Failed to send abort (device may have disconnected)Ta5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}

T8b949e/** Disconnect from the DFU target and cancel the transport coroutine scope. */
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeCatching Tb4b4b4{ Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Error during disconnectTa5d6ff" Tb4b4b4} Tb4b4b4}
Te6edf3transportScopeTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Object-transfer protocol (shared by init packet and firmware)
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Wraps [transferObject] with per-object retry logic. On retry, [transferObject] will re-SELECT the object type and
* resume from the device's reported offset if the CRC matches.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fftransferObjectWithRetryTb4b4b4(
Te6edf3objectTypeTb4b4b4: Tffa657ByteTb4b4b4,
Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4,
Te6edf3onProgressTb4b4b4: Tb4b4b4(Te6edf3suspend Tb4b4b4(Tffa657FloatTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tff7b72?Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72var Te6edf3lastErrorTb4b4b4: Te6edf3Throwable? Tff7b72= Tff7b72null
Te6edf3repeatTb4b4b4(Te6edf3OBJECT_RETRY_COUNTTb4b4b4) Tb4b4b4{ Te6edf3attempt Tff7b72-Tff7b72>
Tff7b72try Tb4b4b4{
Te6edf3transferObjectTb4b4b4(Te6edf3objectTypeTb4b4b4, Te6edf3dataTb4b4b4, Te6edf3onProgressTb4b4b4)
Tff7b72return
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ThrowableTb4b4b4) Tb4b4b4{
Te6edf3lastError Tff7b72= Te6edf3e
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Object transfer failed (attempt Tffd700${Te6edf3attempt Tff7b72+ T79c0ff1Tffd700}Ta5d6ff/Tffd700$Te6edf3OBJECT_RETRY_COUNTTa5d6ff): Tffd700${Te6edf3eTb4b4b4.Te6edf3messageTffd700}Ta5d6ff" Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3attempt Tff7b72< Te6edf3OBJECT_RETRY_COUNT Tff7b72- T79c0ff1Tb4b4b4) Te6edf3delayTb4b4b4(Te6edf3RETRY_DELAYTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72throw Te6edf3lastError Tff7b72?: Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffObject transfer failed after Tffd700$Te6edf3OBJECT_RETRY_COUNTTa5d6ff attemptsTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffLongMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffNestedBlockDepthTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fftransferObjectTb4b4b4(Te6edf3objectTypeTb4b4b4: Tffa657ByteTb4b4b4, Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4, Te6edf3onProgressTb4b4b4: Tb4b4b4(Te6edf3suspend Tb4b4b4(Tffa657FloatTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tff7b72?Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3selectResult Tff7b72= Te6edf3sendSelectTb4b4b4(Te6edf3objectTypeTb4b4b4)
Tff7b72val Te6edf3maxObjectSize Tff7b72= Te6edf3selectResultTb4b4b4.Te6edf3maxSizeTb4b4b4.Te6edf3takeIf Tb4b4b4{ Tffa657it Tff7b72> T79c0ff0 Tb4b4b4} Tff7b72?: Te6edf3DEFAULT_MAX_OBJECT_SIZE
Tff7b72val Te6edf3totalBytes Tff7b72= Te6edf3dataTb4b4b4.Te6edf3size
Tff7b72var Te6edf3offset Tff7b72= T79c0ff0
Tff7b72var Te6edf3isFirstChunk Tff7b72= Tff7b72true
Tff7b72var Te6edf3currentPrnInterval Tff7b72= Tff7b72if Tb4b4b4(Te6edf3objectType Tff7b72=Tff7b72= Te6edf3DfuObjectTypeTb4b4b4.Te6edf3COMMANDTb4b4b4) T79c0ff0 Tff7b72else Te6edf3PRN_INTERVAL

T8b949e// Resume logic β€” per Nordic DFU spec, distinguish between executed objects and partial current object.
Tff7b72if Tb4b4b4(Te6edf3selectResultTb4b4b4.Te6edf3offset Tff7b72in T79c0ff1.Tb4b4b4.Te6edf3totalBytesTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3expectedCrc Tff7b72= Te6edf3DfuCrc32Tb4b4b4.Te6edf3calculateTb4b4b4(Te6edf3dataTb4b4b4, Te6edf3length Tff7b72= Te6edf3selectResultTb4b4b4.Te6edf3offsetTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3expectedCrc Tff7b72=Tff7b72= Te6edf3selectResultTb4b4b4.Te6edf3crc32Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3executedBytes Tff7b72= Te6edf3maxObjectSize Tff7b72* Tb4b4b4(Te6edf3selectResultTb4b4b4.Te6edf3offset Tff7b72/ Te6edf3maxObjectSizeTb4b4b4)
Tff7b72val Te6edf3pendingBytes Tff7b72= Te6edf3selectResultTb4b4b4.Te6edf3offset Tff7b72- Te6edf3executedBytes

Tff7b72if Tb4b4b4(Te6edf3selectResultTb4b4b4.Te6edf3offset Tff7b72=Tff7b72= Te6edf3totalBytesTb4b4b4) Tb4b4b4{
T8b949e// Device already has the complete data. Just execute.
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Device already has all Tffd700$Te6edf3totalBytesTa5d6ff bytes (CRC match), executing...Ta5d6ff" Tb4b4b4}
Te6edf3sendExecuteTb4b4b4(Tb4b4b4)
Te6edf3onProgressTff7b72?.Te6edf3invokeTb4b4b4(T79c0ff1fTb4b4b4)
Tff7b72return
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3pendingBytes Tff7b72=Tff7b72= T79c0ff0 Tff7b72&Tff7b72& Te6edf3executedBytes Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
T8b949e// Offset is at an object boundary β€” last complete object may not be executed yet.
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Resuming at object boundary Tffd700$Te6edf3executedBytesTa5d6ff, executing last object...Ta5d6ff" Tb4b4b4}
Tff7b72try Tb4b4b4{
Te6edf3sendExecuteTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3DfuExceptionTb4b4b4.Te6edf3ProtocolErrorTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3eTb4b4b4.Te6edf3resultCode Tff7b72!Tff7b72= Te6edf3DfuResultCodeTb4b4b4.Te6edf3OPERATION_NOT_PERMITTEDTb4b4b4) Tff7b72throw Te6edf3e
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Execute returned OPERATION_NOT_PERMITTED (already executed), continuing...Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Te6edf3offset Tff7b72= Te6edf3executedBytes
Te6edf3isFirstChunk Tff7b72= Tff7b72false
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3pendingBytes Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
T8b949e// Partial object in progress β€” skip to the start of the current object and resume from there.
T8b949e// We resume from the executed boundary because the partial object needs to be re-sent if we can't
T8b949e// verify the partial state cleanly. The Nordic library does the same thing.
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{
Ta5d6ff"Ta5d6ffDFU: Resuming at offset Tffd700$Te6edf3executedBytesTa5d6ff (executed=Tffd700$Te6edf3executedBytesTa5d6ff, pending=Tffd700$Te6edf3pendingBytesTa5d6ff)Ta5d6ff"
Tb4b4b4}
Te6edf3offset Tff7b72= Te6edf3executedBytes
Te6edf3isFirstChunk Tff7b72= Tff7b72false
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Offset Tffd700${Te6edf3selectResultTb4b4b4.Te6edf3offsetTffd700}Ta5d6ff CRC mismatch β€” restarting from 0Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72while Tb4b4b4(Te6edf3offset Tff7b72< Te6edf3totalBytesTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3objectSize Tff7b72= Te6edf3minOfTb4b4b4(Te6edf3maxObjectSizeTb4b4b4, Te6edf3totalBytes Tff7b72- Te6edf3offsetTb4b4b4)
Te6edf3sendCreateTb4b4b4(Te6edf3objectTypeTb4b4b4, Te6edf3objectSizeTb4b4b4)

T8b949e// First-chunk delay: some older bootloaders need time to prepare flash after Create.
T8b949e// The Nordic DFU library uses 400ms for the first chunk.
Tff7b72if Tb4b4b4(Te6edf3isFirstChunkTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3FIRST_CHUNK_DELAYTb4b4b4)
Te6edf3isFirstChunk Tff7b72= Tff7b72false
Tb4b4b4}

Tff7b72val Te6edf3objectEnd Tff7b72= Te6edf3offset Tff7b72+ Te6edf3objectSize
Te6edf3writePacketsTb4b4b4(Te6edf3dataTb4b4b4, Te6edf3offsetTb4b4b4, Te6edf3objectEndTb4b4b4, Te6edf3currentPrnIntervalTb4b4b4)

Tff7b72val Te6edf3checksumResult Tff7b72= Te6edf3sendCalculateChecksumTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3expectedCrc Tff7b72= Te6edf3DfuCrc32Tb4b4b4.Te6edf3calculateTb4b4b4(Te6edf3dataTb4b4b4, Te6edf3length Tff7b72= Te6edf3objectEndTb4b4b4)

T8b949e// Bytes-lost detection: if the device reports fewer bytes than we sent, some packets were lost in
T8b949e// the BLE stack. Rather than throwing immediately, tighten PRN to 1 and retry the remaining bytes.
Tff7b72if Tb4b4b4(Te6edf3checksumResultTb4b4b4.Te6edf3offset Tff7b72< Te6edf3objectEndTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3bytesLost Tff7b72= Te6edf3objectEnd Tff7b72- Te6edf3checksumResultTb4b4b4.Te6edf3offset
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ffDFU: Tffd700$Te6edf3bytesLostTa5d6ff bytes lost in BLE stack (sent to Tffd700$Te6edf3objectEndTa5d6ff, device at Tffd700${Te6edf3checksumResultTb4b4b4.Te6edf3offsetTffd700}Ta5d6ff)Ta5d6ff"
Tb4b4b4}
T8b949e// Verify CRC up to the device's offset is valid
Tff7b72val Te6edf3partialCrc Tff7b72= Te6edf3DfuCrc32Tb4b4b4.Te6edf3calculateTb4b4b4(Te6edf3dataTb4b4b4, Te6edf3length Tff7b72= Te6edf3checksumResultTb4b4b4.Te6edf3offsetTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3checksumResultTb4b4b4.Te6edf3crc32 Tff7b72!Tff7b72= Te6edf3partialCrcTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ChecksumMismatchTb4b4b4(Te6edf3expected Tff7b72= Te6edf3partialCrcTb4b4b4, Te6edf3actual Tff7b72= Te6edf3checksumResultTb4b4b4.Te6edf3crc32Tb4b4b4)
Tb4b4b4}
T8b949e// Tighten PRN to maximum flow control and resend the lost portion
Te6edf3currentPrnInterval Tff7b72= T79c0ff1
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Forcing PRN=1 and resending from offset Tffd700${Te6edf3checksumResultTb4b4b4.Te6edf3offsetTffd700}Ta5d6ff" Tb4b4b4}
Te6edf3writePacketsTb4b4b4(Te6edf3dataTb4b4b4, Te6edf3checksumResultTb4b4b4.Te6edf3offsetTb4b4b4, Te6edf3objectEndTb4b4b4, Te6edf3currentPrnIntervalTb4b4b4)

Tff7b72val Te6edf3recheckResult Tff7b72= Te6edf3sendCalculateChecksumTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3recheckResultTb4b4b4.Te6edf3offset Tff7b72!Tff7b72= Te6edf3objectEnd Tff7b72|Tff7b72| Te6edf3recheckResultTb4b4b4.Te6edf3crc32 Tff7b72!Tff7b72= Te6edf3expectedCrcTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3expectedHex Tff7b72= Te6edf3expectedCrcTb4b4b4.Te6edf3toUIntTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)
Tff7b72val Te6edf3actualHex Tff7b72= Te6edf3recheckResultTb4b4b4.Te6edf3crc32Tb4b4b4.Te6edf3toUIntTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(
Ta5d6ff"Ta5d6ffRecovery failed after bytes-lost: Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffexpected offset=Tffd700$Te6edf3objectEndTa5d6ff crc=0xTffd700$Te6edf3expectedHexTa5d6ff, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffgot offset=Tffd700${Te6edf3recheckResultTb4b4b4.Te6edf3offsetTffd700}Ta5d6ff crc=0xTffd700$Te6edf3actualHexTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Recovery successful, continuing with PRN=1Ta5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3checksumResultTb4b4b4.Te6edf3offset Tff7b72!Tff7b72= Te6edf3objectEndTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(
Ta5d6ff"Ta5d6ffOffset mismatch after object: expected Tffd700$Te6edf3objectEndTa5d6ff, got Tffd700${Te6edf3checksumResultTb4b4b4.Te6edf3offsetTffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3checksumResultTb4b4b4.Te6edf3crc32 Tff7b72!Tff7b72= Te6edf3expectedCrcTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ChecksumMismatchTb4b4b4(Te6edf3expected Tff7b72= Te6edf3expectedCrcTb4b4b4, Te6edf3actual Tff7b72= Te6edf3checksumResultTb4b4b4.Te6edf3crc32Tb4b4b4)
Tb4b4b4}

T8b949e// Execute with retry for INVALID_OBJECT β€” the SoftDevice may still be erasing flash.
Tff7b72try Tb4b4b4{
Te6edf3sendExecuteTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3DfuExceptionTb4b4b4.Te6edf3ProtocolErrorTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3eTb4b4b4.Te6edf3resultCode Tff7b72=Tff7b72= Te6edf3DfuResultCodeTb4b4b4.Te6edf3INVALID_OBJECT Tff7b72&Tff7b72& Te6edf3offset Tff7b72+ Te6edf3objectSize Tff7b72>Tff7b72= Te6edf3totalBytesTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Execute returned INVALID_OBJECT on final object, retrying once...Ta5d6ff" Tb4b4b4}
Te6edf3delayTb4b4b4(Te6edf3RETRY_DELAYTb4b4b4)
Te6edf3sendExecuteTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4}
Tb4b4b4}

Te6edf3offset Tff7b72= Te6edf3objectEnd
Te6edf3onProgressTff7b72?.Te6edf3invokeTb4b4b4(Te6edf3offsetTb4b4b4.Te6edf3toFloatTb4b4b4(Tb4b4b4) Tff7b72/ Te6edf3totalBytesTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Object complete. Progress: Tffd700$Te6edf3offsetTa5d6ff/Tffd700$Te6edf3totalBytesTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Low-level GATT helpers
T8b949e// ---------------------------------------------------------------------------

T8b949e/**
* Writes [data] from [from] to [until] as MTU-sized packets WITHOUT_RESPONSE.
*
* PRN flow control: every [prnInterval] packets we await a ChecksumResult notification from the device and validate
* the running CRC-32. This prevents the device's receive buffer from overflowing and detects corruption early. Pass
* 0 to disable PRN (used for init packets).
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffwritePacketsTb4b4b4(Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4, Te6edf3fromTb4b4b4: Tffa657IntTb4b4b4, Te6edf3untilTb4b4b4: Tffa657IntTb4b4b4, Te6edf3prnIntervalTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3mtu Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3maximumWriteValueLengthTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSETb4b4b4) Tff7b72?: Te6edf3DEFAULT_BLE_WRITE_VALUE_LENGTH
Tff7b72var Te6edf3pos Tff7b72= Te6edf3from

Te6edf3coroutineScope Tb4b4b4{
T8b949e// Trip-wire: cancels the streaming coroutine the moment Kable observes a disconnect.
Tff7b72val Te6edf3watcher Tff7b72= Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3state Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3first Tb4b4b4{ Tffa657it Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3Disconnected Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffSecure DFU: Link dropped mid-stream at offset Tffd700$Te6edf3posTa5d6ff/Tffd700$Te6edf3untilTa5d6ff (state=Tffd700$Te6edf3stateTa5d6ff)Ta5d6ff" Tb4b4b4}
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffBLE link dropped mid-upload at byte Tffd700$Te6edf3posTa5d6ff/Tffd700$Te6edf3untilTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tff7b72try Tb4b4b4{
Tff7b72var Te6edf3packetsSincePrn Tff7b72= T79c0ff0
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4, Te6edf3timeout Tff7b72= Te6edf3STREAM_TIMEOUTTb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3packetChar Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3characteristicTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3PACKETTb4b4b4)
Tff7b72while Tb4b4b4(Te6edf3pos Tff7b72< Te6edf3untilTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3chunkEnd Tff7b72= Te6edf3minOfTb4b4b4(Te6edf3pos Tff7b72+ Te6edf3mtuTb4b4b4, Te6edf3untilTb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3packetCharTb4b4b4, Te6edf3dataTb4b4b4.Te6edf3copyOfRangeTb4b4b4(Te6edf3posTb4b4b4, Te6edf3chunkEndTb4b4b4)Tb4b4b4, Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSETb4b4b4)
Te6edf3pos Tff7b72= Te6edf3chunkEnd
Te6edf3packetsSincePrnTff7b72+Tff7b72+

Tff7b72if Tb4b4b4(Te6edf3prnInterval Tff7b72> T79c0ff0 Tff7b72&Tff7b72& Te6edf3packetsSincePrn Tff7b72>Tff7b72= Te6edf3prnInterval Tff7b72&Tff7b72& Te6edf3pos Tff7b72< Te6edf3untilTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3response Tff7b72= Te6edf3awaitNotificationTb4b4b4(Te6edf3COMMAND_TIMEOUTTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3response Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3ChecksumResultTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3expectedCrc Tff7b72= Te6edf3DfuCrc32Tb4b4b4.Te6edf3calculateTb4b4b4(Te6edf3dataTb4b4b4, Te6edf3length Tff7b72= Te6edf3posTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3responseTb4b4b4.Te6edf3offset Tff7b72!Tff7b72= Te6edf3pos Tff7b72|Tff7b72| Te6edf3responseTb4b4b4.Te6edf3crc32 Tff7b72!Tff7b72= Te6edf3expectedCrcTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ChecksumMismatchTb4b4b4(Te6edf3expected Tff7b72= Te6edf3expectedCrcTb4b4b4, Te6edf3actual Tff7b72= Te6edf3responseTb4b4b4.Te6edf3crc32Tb4b4b4)
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: PRN checksum OK at offset Tffd700$Te6edf3posTa5d6ff" Tb4b4b4}
Tb4b4b4}
Te6edf3packetsSincePrn Tff7b72= T79c0ff0
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3watcherTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffsendCommandTb4b4b4(Te6edf3payloadTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3DfuResponse Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3controlChar Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3characteristicTb4b4b4(Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3CONTROL_POINTTb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3writeTb4b4b4(Te6edf3controlCharTb4b4b4, Te6edf3payloadTb4b4b4, Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSETb4b4b4)
Tb4b4b4}
Tff7b72return Te6edf3awaitNotificationTb4b4b4(Te6edf3COMMAND_TIMEOUTTb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffsetPrnTb4b4b4(Te6edf3valueTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3payload Tff7b72= Te6edf3byteArrayOfTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3SET_PRNTb4b4b4) Tff7b72+ Te6edf3intToLeBytesTb4b4b4(Te6edf3valueTb4b4b4)Tb4b4b4.Te6edf3copyOfRangeTb4b4b4(T79c0ff0Tb4b4b4, T79c0ff2Tb4b4b4)
Tff7b72val Te6edf3response Tff7b72= Te6edf3sendCommandTb4b4b4(Te6edf3payloadTb4b4b4)
Te6edf3responseTb4b4b4.Te6edf3requireSuccessTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3SET_PRNTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: PRN set to Tffd700$Te6edf3valueTa5d6ff" Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffsendSelectTb4b4b4(Te6edf3objectTypeTb4b4b4: Tffa657ByteTb4b4b4)Tb4b4b4: Te6edf3DfuResponseTb4b4b4.Te6edf3SelectResult Tb4b4b4{
Tff7b72val Te6edf3response Tff7b72= Te6edf3sendCommandTb4b4b4(Te6edf3byteArrayOfTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3SELECTTb4b4b4, Te6edf3objectTypeTb4b4b4)Tb4b4b4)
Tff7b72return Tff7b72when Tb4b4b4(Te6edf3responseTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3SelectResult Tff7b72-Tff7b72> Te6edf3response

Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3Failure Tff7b72-Tff7b72>
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ProtocolErrorTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3SELECTTb4b4b4, Te6edf3responseTb4b4b4.Te6edf3resultCodeTb4b4b4, Te6edf3responseTb4b4b4.Te6edf3extendedErrorTb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffUnexpected response to SELECT: Tffd700$Te6edf3responseTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffsendCreateTb4b4b4(Te6edf3objectTypeTb4b4b4: Tffa657ByteTb4b4b4, Te6edf3sizeTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3payload Tff7b72= Te6edf3byteArrayOfTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3CREATETb4b4b4, Te6edf3objectTypeTb4b4b4) Tff7b72+ Te6edf3intToLeBytesTb4b4b4(Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3response Tff7b72= Te6edf3sendCommandTb4b4b4(Te6edf3payloadTb4b4b4)
Te6edf3responseTb4b4b4.Te6edf3requireSuccessTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3CREATETb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Created object type=0xTffd700${Te6edf3objectTypeTb4b4b4.Te6edf3toUByteTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff size=Tffd700$Te6edf3sizeTa5d6ff" Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffsendCalculateChecksumTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3DfuResponseTb4b4b4.Te6edf3ChecksumResult Tb4b4b4{
Tff7b72val Te6edf3response Tff7b72= Te6edf3sendCommandTb4b4b4(Te6edf3byteArrayOfTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3CALCULATE_CHECKSUMTb4b4b4)Tb4b4b4)
Tff7b72return Tff7b72when Tb4b4b4(Te6edf3responseTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3ChecksumResult Tff7b72-Tff7b72> Te6edf3response

Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3Failure Tff7b72-Tff7b72>
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ProtocolErrorTb4b4b4(
Te6edf3DfuOpcodeTb4b4b4.Te6edf3CALCULATE_CHECKSUMTb4b4b4,
Te6edf3responseTb4b4b4.Te6edf3resultCodeTb4b4b4,
Te6edf3responseTb4b4b4.Te6edf3extendedErrorTb4b4b4,
Tb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffUnexpected response to CALCULATE_CHECKSUM: Tffd700$Te6edf3responseTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffsendExecuteTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3response Tff7b72= Te6edf3sendCommandTb4b4b4(Te6edf3byteArrayOfTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3EXECUTETb4b4b4)Tb4b4b4)
Te6edf3responseTb4b4b4.Te6edf3requireSuccessTb4b4b4(Te6edf3DfuOpcodeTb4b4b4.Te6edf3EXECUTETb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Object executed.Ta5d6ff" Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffawaitNotificationTb4b4b4(Te6edf3timeoutTb4b4b4: Te6edf3DurationTb4b4b4)Tb4b4b4: Te6edf3DfuResponse Tff7b72= Tff7b72try Tb4b4b4{
Te6edf3withTimeoutTb4b4b4(Te6edf3timeoutTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3bytes Tff7b72= Te6edf3notificationChannelTb4b4b4.Te6edf3receiveTb4b4b4(Tb4b4b4)
Te6edf3DfuResponseTb4b4b4.Te6edf3parseTb4b4b4(Te6edf3bytesTb4b4b4)Tb4b4b4.Te6edf3also Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffDFU: Notification β†’ Tffd700$Te6edf3itTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3_Tb4b4b4: Te6edf3TimeoutCancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TimeoutTb4b4b4(Ta5d6ff"Ta5d6ffNo response from Control Point after Tffd700$Te6edf3timeoutTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Te6edf3DfuResponseTb4b4b4.Td2a8ffrequireSuccessTb4b4b4(Te6edf3expectedOpcodeTb4b4b4: Tffa657ByteTb4b4b4) Tb4b4b4{
Tff7b72when Tb4b4b4(Tff7b72thisTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3Success Tff7b72-Tff7b72>
Tff7b72if Tb4b4b4(Te6edf3opcode Tff7b72!Tff7b72= Te6edf3expectedOpcodeTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(
Ta5d6ff"Ta5d6ffResponse opcode mismatch: expected 0xTffd700${Te6edf3expectedOpcodeTb4b4b4.Te6edf3toUByteTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffgot 0xTffd700${Te6edf3opcodeTb4b4b4.Te6edf3toUByteTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72is Te6edf3DfuResponseTb4b4b4.Te6edf3Failure Tff7b72-Tff7b72> Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3ProtocolErrorTb4b4b4(Te6edf3opcodeTb4b4b4, Te6edf3resultCodeTb4b4b4, Te6edf3extendedErrorTb4b4b4)

Tff7b72else Tff7b72-Tff7b72>
Tff7b72throw Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(
Ta5d6ff"Ta5d6ffUnexpected response for opcode 0xTffd700${Te6edf3expectedOpcodeTb4b4b4.Te6edf3toUByteTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toStringTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tffd700}Ta5d6ff: Tffd700$Te6edf3thisTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// ---------------------------------------------------------------------------
T8b949e// Scanning helpers
T8b949e// ---------------------------------------------------------------------------

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffscanForDeviceTb4b4b4(Te6edf3predicateTb4b4b4: Tb4b4b4(Te6edf3BleDeviceTb4b4b4) Tff7b72-Tff7b72> Tffa657BooleanTb4b4b4)Tb4b4b4: Te6edf3BleDevice? Tff7b72= Te6edf3scanForBleDeviceTb4b4b4(
Te6edf3scanner Tff7b72= Te6edf3scannerTb4b4b4,
Te6edf3tag Tff7b72= Ta5d6ff"Ta5d6ffDFUTa5d6ff"Tb4b4b4,
Te6edf3serviceUuid Tff7b72= Te6edf3SecureDfuUuidsTb4b4b4.Te6edf3SERVICETb4b4b4,
Te6edf3retryCount Tff7b72= Te6edf3SCAN_RETRY_COUNTTb4b4b4,
Te6edf3retryDelay Tff7b72= Te6edf3SCAN_RETRY_DELAYTb4b4b4,
Te6edf3predicate Tff7b72= Te6edf3predicateTb4b4b4,
Tb4b4b4)

T8b949e// ---------------------------------------------------------------------------
T8b949e// Constants
T8b949e// ---------------------------------------------------------------------------

Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3CONNECT_TIMEOUT Tff7b72= T79c0ff1T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3COMMAND_TIMEOUT Tff7b72= T79c0ff3T79c0ff0.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3SUBSCRIPTION_SETTLE Tff7b72= T79c0ff5T79c0ff0T79c0ff0.Te6edf3milliseconds
Tff7b72private Tff7b72val Te6edf3BUTTONLESS_RESPONSE_TIMEOUT Tff7b72= T79c0ff3.Te6edf3seconds

T8b949e/**
* Tight wall-clock cap on the buttonless trigger phase (Secure or Legacy). After we send the opcode, the device
* reboots into the bootloader and disconnects β€” but Kable's `WITH_RESPONSE` write blocks on an ATT ACK that
* never arrives in this race. If we don't see the disconnect propagate within this window, the device almost
* certainly didn't receive the byte, so failing fast lets the caller surface a useful error (or retry) instead
* of waiting on the default 30s `profile()` timeout. Comfortably exceeds the secure-path indication wait
* ([BUTTONLESS_RESPONSE_TIMEOUT]) plus settle/write overhead.
*/
Tff7b72private Tff7b72val Te6edf3TRIGGER_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72const Tff7b72val Te6edf3SCAN_RETRY_COUNT Tff7b72= T79c0ff3
Tff7b72private Tff7b72val Te6edf3SCAN_RETRY_DELAY Tff7b72= T79c0ff2.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3RETRY_DELAY Tff7b72= T79c0ff2.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3FIRST_CHUNK_DELAY Tff7b72= T79c0ff4T79c0ff0T79c0ff0.Te6edf3milliseconds

T8b949e/**
* Wall-clock budget for a single firmware-object streaming session. Must comfortably exceed the upload duration
* for the largest expected image at the slowest realistic Secure DFU throughput. Per-PRN/per-write watchdogs
* inside the loop detect real stalls; this is just a safety net so a hung profile block can't sit forever.
*/
Tff7b72private Tff7b72val Te6edf3STREAM_TIMEOUT Tff7b72= T79c0ff1T79c0ff5.Te6edf3minutes

T8b949e/** Response code prefix for Buttonless DFU indications (0x20 = response). */
Tff7b72private Tff7b72const Tff7b72val Te6edf3BUTTONLESS_RESPONSE_CODETb4b4b4: Tffa657Byte Tff7b72= T79c0ff0Te6edf3x20

T8b949e/**
* PRN interval: device sends a ChecksumResult notification every N packets. Provides flow control and early CRC
* validation. 0 = disabled.
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3PRN_INTERVAL Tff7b72= T79c0ff1T79c0ff0

T8b949e/** Number of times to retry a failed object transfer before giving up. */
Tff7b72private Tff7b72const Tff7b72val Te6edf3OBJECT_RETRY_COUNT Tff7b72= T79c0ff3

Tff7b72private Tff7b72const Tff7b72val Te6edf3DEFAULT_MAX_OBJECT_SIZE Tff7b72= T79c0ff4T79c0ff0T79c0ff9T79c0ff6
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.19s